Simple Observation in Django: How Can I Correctly Modify The `attrs` sent to __new__ of a Django Mod

Posted by DGGenuine on Stack Overflow See other posts from Stack Overflow or by DGGenuine
Published on 2010-05-03T23:02:22Z Indexed on 2010/05/03 23:08 UTC
Read the original article Hit count: 498

Hello,

I'm a strong proponent of the observer pattern, and this is what I'd like to be able to do in my Django models.py:

class AModel(Model):
    __metaclass__ = SomethingMagical

    @post_save(AnotherModel)
    @classmethod
    def observe_another_model_saved(klass, sender, instance, created, **kwargs):
        pass

    @pre_init('YetAnotherModel')
    @classmethod
    def observe_yet_another_model_initializing(klass, sender, *args, **kwargs):
        pass

    @post_delete('DifferentApp.SomeModel')
    @classmethod
    def observe_some_model_deleted(klass, sender, **kwargs):
        pass

This would connect a signal with sender = the decorator's argument and receiver = the decorated method. Right now my signal connection code all exists in __init__.py which is okay, but a little unmaintainable. I want this code all in one place, the models.py file.

Thanks to helpful feedback from the community I'm very close (I think.) (I'm using a metaclass solution instead of the class decorator solution in the previous question/answer because you can't set attributes on classmethods, which I need.)

I am having a strange error I don't understand. At the end of my post are the contents of a models.py that you can pop into a fresh project/application to see the error. Set your database to sqlite and add the application to installed apps.

This is the error:

Validating models...
Unhandled exception in thread started by 
Traceback (most recent call last):
  File "/Library/Python/2.6/site-packages//lib/python2.6/site-packages/django/core/management/commands/runserver.py", line 48, in inner_run
  File "/Library/Python/2.6/site-packages/django/core/management/base.py", line 253, in validate
    raise CommandError("One or more models did not validate:\n%s" % error_text)
django.core.management.base.CommandError: One or more models did not validate:
local.myothermodel: 'my_model' has a relation with model MyModel, which has either not been installed or is abstract.

I've indicated a few different things you can comment in/out to fix the error. First, if you don't modify the attrs sent to the metaclass's __new__, then the error does not arise. (Note even if you copy the dictionary element by element into a new dictionary, it still fails; only using the exact attrs dictionary works.) Second, if you reference the first model by class rather than by string, the error also doesn't arise regardless of what you do in __new__.

I appreciate your help. I'll be githubbing the solution if and when it works. Maybe other people would enjoy a simplified way to use Django signals to observe application happenings.

#models.py
from django.db import models
from django.db.models.base import ModelBase
from django.db.models import signals
import pdb

class UnconnectedMethodWrapper(object):
    sender = None
    method = None
    signal = None

    def __init__(self, signal, sender, method):
        self.signal = signal
        self.sender = sender
        self.method = method

def post_save(sender):
    return _make_decorator(signals.post_save, sender)

def _make_decorator(signal, sender):
    def decorator(view):
        return UnconnectedMethodWrapper(signal, sender, view)
    return decorator

class ConnectableModel(ModelBase):
    """
    A meta class for any class that will have static or class methods
    that need to be connected to signals.
    """

    def __new__(cls, name, bases, attrs):

        unconnecteds = {}

        ## NO WORK
        newattrs = {}
        for name, attr in attrs.iteritems():
            if isinstance(attr, UnconnectedMethodWrapper):
                unconnecteds[name] = attr
                newattrs[name] = attr.method #replace the UnconnectedMethodWrapper with the method it wrapped.
            else:
                newattrs[name] = attr

        ## NO WORK
        # newattrs = {}
        # for name, attr in attrs.iteritems():
        #   newattrs[name] = attr

        ## WORKS
        # newattrs = attrs

        new = super(ConnectableModel, cls).__new__(cls, name, bases, newattrs)

        for name, unconnected in unconnecteds.iteritems():
            _connect_signal(unconnected.signal, unconnected.sender, getattr(new, name), new._meta.app_label)

        return new

def _connect_signal(signal, sender, receiver, default_app_label):
    # full implementation also accepts basestring as sender and will look up model accordingly
    signal.connect(sender=sender, receiver=receiver)

class MyModel(models.Model):
    __metaclass__ = ConnectableModel

    @post_save('In my application this string matters')
    @classmethod
    def observe_it(klass, sender, instance, created, **kwargs):
        pass

    @classmethod
    def normal_class_method(klass):
        pass

class MyOtherModel(models.Model):

    ## WORKS
    # my_model = models.ForeignKey(MyModel)

    ## NO WORK
    my_model = models.ForeignKey('MyModel')

© Stack Overflow or respective owner

Related posts about django

Related posts about django-signals